﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WPFEdit.Views;
using System.ComponentModel;
using System.Windows.Forms;

/// <summary>
/// The Task model lets you specify tasks as a list of steps. The TaskManager has
/// methods to start a task in either synchonous or asynchrounous mode, automatically showing a
/// cancellable progress dialog box for sync. tasks.
/// </summary>
namespace WPFEdit.Framework
{
	
	/// <summary>
	/// The Task Exception will be thrown by the Task Manager when any of the code called by a task step throws 
	/// an unhandled exception.
	/// </summary>
    public class TaskException : System.Exception
    {
        private Exception m_stepException;

        public Exception StepException
        {
            get { return m_stepException; }
            set { m_stepException = value; }
        }
    }

	/// <summary>
	/// The ParalellTaskStepException is thrown by the task Manager when a Paralell task step throws an
	/// exception in one of its sub steps.
	/// </summary>
	public class ParalellTaskStepException : System.Exception
	{
		internal List<Tuple<int, Exception>> m_subStepExceptions = new List<Tuple<int,Exception>>();

		/// <summary>
		/// Gets a list of 0-start sub-step index/exception pairings
		/// </summary>
		public IEnumerable<Tuple<int, Exception>> SubStepExceptions
		{
			get { return m_subStepExceptions; }
		}

		internal void AddException(Exception ex, int stepNumber)
		{
			m_subStepExceptions.Add(new Tuple<int,Exception>(stepNumber, ex));
		}
	}

	/// <summary>
	/// Event arguments for the Task Completed event.
	/// </summary>
    public class TaskCompleteEventArgs
    {
        public readonly Exception Error;
        public readonly bool Canceled;
        public readonly object Result;

        public TaskCompleteEventArgs(Exception error, bool canceled, object result)
        {
            Error = error;
            Canceled = canceled;
            Result = result;
        }
    }

    public delegate void TaskCompletedEventHandler(object sender, TaskCompleteEventArgs e);

	/// <summary>
	/// Defines a unit of work to be done on a separate thread.
	/// </summary>
    public class TaskStep
    {
        private string m_statusText;
        private double m_valueIncrement;
        private Action m_action;
        private TimeSpan m_timeout = TimeSpan.FromSeconds(30);
        private Action m_UIThreadPreWork;
        private Action m_rollback;
        
        /// <summary>
        /// Method to be called on the UI thread before a new thread is created to
        /// do the main work. Should do as little work as possible here.
        /// </summary>
        public Action UIThreadPreWork
        {
            get { return m_UIThreadPreWork; }
            set { m_UIThreadPreWork = value; }
        }

        /// <summary>
        /// Method called on the UI thread to undo wotk done by this step
        /// when it's Task's Rollback method is called
        /// </summary>
        public Action OnRollback
        {
            get { return m_rollback; }
            set { m_rollback = value; }
        }

		/// <summary>
		/// The text to show on the progress dialog while this step is running.
		/// </summary>
        public string StatusText
        {
            get { return m_statusText; }
            set { m_statusText = value; }
        }

        /// <summary>
        /// The action for the step to perform. Note that this code cannot modify any UI
        /// elements, nor can it create anu objects that will act as a DataContext for
        /// a UI element. Use UIThreadPreWork for these kinds of calls
        /// </summary>
        public Action StepAction
        {
            get { return m_action; }
            set { m_action = value; }
        }
    }

	/// <summary>
	/// A special kind of Task Step that is definied by a set of substeps that will each be run
	/// in paralell when a Task reaches this step. The task will not move to the next step
	/// until all of this step's substeps have finished.
	/// </summary>
	public class ParallelTaskStep : TaskStep
	{
		private List<TaskStep> m_stepList = new List<TaskStep>();
		private int m_nextStepNumber = 0;

		/// <summary>
		/// Adds a step to be ran in paralell when this step is ran. The step cannot also be a paralell step.
		/// </summary>
		/// <param name="step"></param>
		public void AddStep(TaskStep step)
		{
			if (step.GetType() == typeof(ParallelTaskStep)) throw new Exception("Cannot add a paralell task step to a paralell task step.");
			m_stepList.Add(step);
		}

		/// <summary>
		/// Gets the next paralell task step, or throws an exception if there are no more steps.
		/// </summary>
		/// <returns></returns>
		public TaskStep GetNextStep()
		{
			if (HasNextStep)
			{
				TaskStep s = m_stepList[m_nextStepNumber];
				++m_nextStepNumber;
				return s;
			}
			throw new InvalidOperationException("No more steps exist");
		}

		/// <summary>
		/// Resets the current step number to 0.
		/// </summary>
		public void Reset()
		{
			m_nextStepNumber = 0;
		}

		/// <summary>
		/// Returs true if there are more substeps.
		/// </summary>
		public bool HasNextStep
		{
			get { return m_nextStepNumber != m_stepList.Count; }
		}

		/// <summary>
		/// Retruns the next 0-start sub step index.
		/// </summary>
		public int NextStepNumber
		{
			get { return m_nextStepNumber; }
		}

		/// <summary>
		/// Returns the number of substeps.
		/// </summary>
		public int NumSteps
		{
			get { return m_stepList.Count; }
		}
	}

	/// <summary>
	/// The task encapsulates a sequence of work units through task Steps.
	/// </summary>
    public class Task : INotifyPropertyChanged
    {
        private List<TaskStep> m_steps = new List<TaskStep>();
        private string m_windowTitle;
        private string m_statusText;
        private double m_statusValue = 0.0;
        private int m_currentStepNumber = 0;

		/// <summary>
		/// Adds a step to the end of the list. Can be called during a step (not thread safe).
		/// </summary>
		/// <param name="step"></param>
        public void AddStep(TaskStep step)
        {
            m_steps.Add(step);
        }

		/// <summary>
		/// Adds a step to the list in the position after the currently running step. Can be called during a step (not thread safe).
		/// </summary>
		/// <param name="step"></param>
        public void AddStepAsNext(TaskStep step)
        {
            m_steps.Insert(m_currentStepNumber, step);
        }

        /// <summary>
        /// Adds a step. The action for the step to perform cannot modify any UI
        /// elements, nor can it create anu objects that will act as a DataContext for
        /// a UI element. Use the manual Step Constructor with UIThreadPreWork for these kinds of calls.
        /// </summary>
        public void AddStep(string stepName, Action stepAction)
        {
            m_steps.Add(new TaskStep() { StatusText = stepName, StepAction = stepAction });
        }

		/// <summary>
		/// Sets the current step to be the beginning
		/// </summary>
        public void Reset()
        {
            m_currentStepNumber = 0;
			m_statusValue = 0.0;
        }

		/// <summary>
		/// Returns the 1-starting index of the step that is currently running.
		/// </summary>
        public int CurrentStepNumber
        {
            get { return m_currentStepNumber; }
        }

		/// <summary>
		/// The title to put on the progress dialog.
		/// </summary>
        public string WindowTitle
        {
            get { return m_windowTitle; }
            set { m_windowTitle = value; }
        }

		/// <summary>
		/// The text shown in the dialog's status area. Can be updated by steps.
		/// </summary>
        public string StatusText
        {
            get { return m_statusText; }
            set
            {
                if (m_statusText != value)
                {
                    m_statusText = value;
                    if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("StatusText"));
                }
            }
        }

		/// <summary>
		/// The value used to set the progress dialog's status bar magnitude.
		/// </summary>
        public double StatusValue
        {
            get { return m_statusValue; }
            set
            {
                if (m_statusValue != value)
                {
                    m_statusValue = value;
                    if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("StatusValue"));
                }
            }
        }

		/// <summary>
		/// Returns the next step, or throws an exception is there is none.
		/// </summary>
		/// <returns></returns>
        public TaskStep GetNextStep()
        {
            if (HasNextStep)
            {
                TaskStep s = m_steps[m_currentStepNumber];
                ++m_currentStepNumber;
                return s;
            }
            throw new InvalidOperationException("No more steps exist");
        }

		/// <summary>
		/// Returns whether there are any more steps to run.
		/// </summary>
        public bool HasNextStep
        {
            get { return m_currentStepNumber != m_steps.Count; }
        }

		/// <summary>
		/// Triggers the Task Completed event.
		/// </summary>
		/// <param name="error">Any error that was thrown during execution</param>
		/// <param name="canceled">Whether the task was cancelled before finihsing</param>
		/// <param name="result">The result, if any.</param>
        public void OnTaskCompleted(Exception error, bool canceled, object result)
        {
            if (TaskCompleted != null)
            {
                TaskCompleted(this, new TaskCompleteEventArgs(error,canceled, result));
            }
        }

		/// <summary>
		/// Calls each step's Rollback method in backwards sequence, starting from the most recently finished step.
		/// </summary>
        public void Rollback()
        {
            if (m_currentStepNumber == 0) return;
			m_currentStepNumber--;
            for (; m_currentStepNumber >= 0; m_currentStepNumber--)
            {
                TaskStep s = m_steps[m_currentStepNumber];
                if (s.OnRollback != null)
                {
                    s.OnRollback();
                }
            }
        }

		public int NumSteps
		{
			get { return m_steps.Count; }
		}

        public event PropertyChangedEventHandler PropertyChanged;
        public event TaskCompletedEventHandler TaskCompleted;
    }

	/// <summary>
	/// A class to run tasks. This class can be used multiple times for different tasks,
	/// but cannot be used for multiple tasks in paralell.
	/// </summary>
    public class TaskManager
    {
        private ProgressDialog m_taskDialog;
        private Task m_task;
        private Exception m_error;
        private bool m_started = false;
        private bool m_canceled = false;
		private bool m_indeterminate;
        private object m_result;
		private int m_lastNumSteps;
		private double m_lastValueIncr;
		private static double s_maxProgressValue = 100.0;

		private ParallelTaskStep m_paralellTaskStep;
		private ParalellTaskStepException m_parExcpetion;
		private int m_numParalellSubStepsDone = -1;
		private object m_subStepCountLock = new object();

		/// <summary>
		/// Runs a task, showing a cancellable progress dialog and does not return until either the task finishes, the 
		/// task in cancelled, or a task step throws an exception. If the task is cancelled, the current step will have to finish
		/// before this returns. This will re-throw any exception that a task step throws, but this will repackage it as a TaskException.
		/// 
		/// </summary>
		/// <param name="indeterminate">Whether to make the progress bar indeterminate</param>
		/// <param name="taskInfo">The task to run</param>
		/// <returns>true if the task finished without being cancelled, false otherwise</returns>
        public bool RunSynchronousTask(bool indeterminate, Task taskInfo)
        {
			Reset();
            ProgressDialog dlg = new ProgressDialog();
            dlg.DataContext = taskInfo;
            dlg.IsIndeterminate = indeterminate;
            m_taskDialog = dlg;
            m_task = taskInfo;
			m_indeterminate = indeterminate;
            dlg.Closing += OnCancel;
			dlg.Activated += OnDlgActivated;
            dlg.ShowDialog();
            taskInfo.OnTaskCompleted(m_error, m_canceled, m_result);

            if (m_error != null)
            {
                throw m_error;
            }
            return !m_canceled;
        }

		private void OnDlgActivated(object sender, EventArgs e)
		{
			if (!m_started)
			{
				m_taskDialog.Activated -= OnDlgActivated;
				m_started = true; RunNextStep();
			}
		}

		private void Reset()
		{
			m_task = null;
			m_taskDialog = null;
			m_error = null;
			m_started = false;
			m_canceled = false;
			m_numParalellSubStepsDone = -1;
			m_paralellTaskStep = null;
			m_parExcpetion = null;
			m_lastNumSteps = 0;
			m_lastValueIncr = 0;
		}

		/// <summary>
		/// Sets a task to be ran, then immediately returns. The result of the task can be found using the
		/// task's TaskCompleted event.
		/// </summary>
		/// <param name="taskInfo"></param>
        public void RunBackgroundTask(Task taskInfo)
        {
            m_task = taskInfo;
            RunNextStep();
        }

        void OnCancel(object sender, CancelEventArgs e)
        {
            if (m_taskDialog.TaskDone) return; //not a cancel
            m_canceled = true;
            m_taskDialog.StatusText = "Canceling, waiting for current step to finish...";
        }

		/// <summary>
		/// Cancels the current task. Can be called from a step.
		/// </summary>
		public void CancelTask()
		{
			m_canceled = true;
		}

		void RunStep(TaskStep step)
		{
			BackgroundWorker worker = new BackgroundWorker();
			int parTaskStep = -1;
			if (m_paralellTaskStep != null)
			{
				parTaskStep = m_paralellTaskStep.NextStepNumber - 1;
			}
			if (!m_indeterminate && m_taskDialog != null)
			{
				if (m_lastNumSteps != m_task.NumSteps)
				{
					double valueLeft = s_maxProgressValue - m_task.StatusValue;
					m_lastValueIncr = m_task.NumSteps != m_task.CurrentStepNumber - 1 ? valueLeft / (m_task.NumSteps - (m_task.NumSteps - m_task.CurrentStepNumber - 1)) : valueLeft;
					m_lastNumSteps = m_task.NumSteps;
				}
				m_task.StatusValue = m_task.StatusValue + m_lastValueIncr;
			}
			if (step.UIThreadPreWork != null)
			{
				try
				{
					step.UIThreadPreWork();
				}
				catch (Exception ex)
				{
					HandleException(ex,parTaskStep);
					return;
				}
			}
			worker.DoWork += (sender, e) =>
			{
				//Background worker is supposed to catch exceptions
				//automatically and assign them to the e.error field in
				//runworkercompleted args. But it's just showing an unhandled exception.
				//So this workaround passes the exception as the result...
				try
				{
					if (step.StepAction != null)
					{
						step.StepAction();
					}
				}
				catch (Exception ex)
				{
					e.Result = ex;
				}
			};
			worker.RunWorkerCompleted +=
				(sender, e) =>
				{
					if (e.Error != null || e.Cancelled)
					{
						if (e.Error != null)
						{
							HandleException(e.Error, parTaskStep);
						}
						else
						{
							m_canceled = true;
							EndTask();
						}
					}
					else if (e.Result != null && typeof(Exception).IsAssignableFrom(e.Result.GetType()))
					{
						HandleException((Exception)e.Result, parTaskStep);
					}
					else if (!m_canceled)
					{
						if (m_paralellTaskStep != null)
						{
							RunParalellStep(m_paralellTaskStep);
						}
						else
						{
							RunNextStep();
						}
					}
					else
					{
						m_result = e.Result;
						EndTask();
					}
					if (m_paralellTaskStep != null)
					{
						OnParallelStepCompleted(e);
					}
				};
			worker.RunWorkerAsync();
		}

		private void HandleException(Exception ex, int parStepNum)
		{
			if (m_paralellTaskStep == null || m_numParalellSubStepsDone < 0) //If num stepsdone is -1, this is the pre-step before the paralell steps
			{
				var err = new TaskException();
				m_error = err;
				err.StepException = ex;
				m_paralellTaskStep = null;//set to null to override logic of not ending task during par step.
			}
			else
			{
				m_error = m_parExcpetion;
				m_parExcpetion.AddException(ex, parStepNum);
			}
			EndTask();
		}

		private void OnParallelStepCompleted(RunWorkerCompletedEventArgs e)
		{
			lock (m_subStepCountLock)
			{
				m_numParalellSubStepsDone++;
			
				if (m_numParalellSubStepsDone == m_paralellTaskStep.NumSteps)
				{
					if (m_error != null || m_canceled)
					{
						m_paralellTaskStep = null;
						EndTask();
					}
					else
					{
						RunNextStep();
					}
				}
			}
		}

        void RunNextStep()
        {
            if (m_task.HasNextStep)
            {
                TaskStep step = m_task.GetNextStep();

                if (m_taskDialog != null)
                {
                    m_taskDialog.StatusText = step.StatusText;
                }

				m_paralellTaskStep = step as ParallelTaskStep;
				if (m_paralellTaskStep != null)
				{
					m_parExcpetion = new ParalellTaskStepException();
				}
				m_numParalellSubStepsDone = -1;

				RunStep(step);
                
            }
            else
            {
                m_paralellTaskStep = null;
                EndTask();
            }
        }

		private void RunParalellStep(ParallelTaskStep pStep)
		{
			if (m_numParalellSubStepsDone >= 0)
			{
				return; //renetry by a paralell taskstep's substep. Ignore.
			}

			m_paralellTaskStep = pStep;
			m_numParalellSubStepsDone = 0;

			if (pStep.HasNextStep)
			{
				while (pStep.HasNextStep)
				{
					TaskStep step = pStep.GetNextStep();
					RunStep(step);
				}
			}
			else
			{
				RunNextStep();
			}
		}

        private void EndTask()
        {
			if (m_paralellTaskStep == null)
			{
				if (m_taskDialog != null)
				{
					m_taskDialog.TaskDone = true;
					m_taskDialog.Close();
				}
				else
				{
					m_task.OnTaskCompleted(m_error, m_canceled, m_result);
				}
			}
        }


        
    }
}
